import type { Object3D } from 'three';
import { useLogger } from './useLogger';

export interface TresLoader<T> extends THREE.Loader {
    load (
        url: string,
        onLoad?: ( result: T ) => void,
        onProgress?: ( event: ProgressEvent ) => void,
        onError?: ( event: ErrorEvent ) => void,
    ): unknown;
    loadAsync ( url: string, onProgress?: ( event: ProgressEvent ) => void ): Promise<T>;
}

export type LoaderProto<T> = new ( ...args: any ) => TresLoader<T extends unknown ? any : T>;
export type LoaderReturnType<T, L extends LoaderProto<T>> = T extends unknown
    ? Awaited<ReturnType<InstanceType<L>[ 'loadAsync' ]>>
    : T;

/**
 * Traverse an object and return all the nodes and materials
 *
 * @export
 * @param {Object3D} object
 * @return { [key: string]: any }
 */
export function traverseObjects ( object: Object3D ) {
    const data: { [ key: string ]: any; } = { nodes: {}, materials: {} };
    if ( object ) {
        object.traverse( ( obj: any ) => {
            if ( obj.name ) {
                data.nodes[ obj.name ] = obj;
            }
            if ( obj.material && !data.materials[ obj.material.name ] ) {
                data.materials[ obj.material.name ] = obj.material;
            }
        } );
    }
    return data;
}

export type Extensions<T extends { prototype: LoaderProto<any>; }> = ( loader: T[ 'prototype' ] ) => void;

/**
 * Load resources using THREE loaders and return the result as a promise
 *
 * @see https://tresjs.org/api/composables.html#useloader
 * @see https://threejs.org/docs/index.html?q=loader#api/en/loaders/Loader
 *
 * ```ts
 * import { useLoader } from '@tresjs/core'
 * import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
 *
 * const { scene } = await useLoader(THREE.GLTFLoader, 'path/to/asset.gltf')
 * ```
 *
 * @export
 * @template T
 * @template U
 * @param {T} Loader
 * @param {U} url
 * @param {Extensions<T>} [extensions]
 * @param {(event: ProgressEvent<EventTarget>) => void} [onProgress]
 * @param {(proto: TresLoader<T>) => void} [cb]
 * @return {*}
 */
export async function useLoader<T extends LoaderProto<T>, U extends string | string[]> (
    Loader: T,
    url: U,
    extensions?: Extensions<T>,
    onProgress?: ( event: ProgressEvent<EventTarget> ) => void,
    cb?: ( proto: TresLoader<T> ) => void,
) {
    const { logError } = useLogger();
    const proto = new Loader();
    if ( cb ) {
        cb( proto );
    }

    if ( extensions ) {
        extensions( proto );
    }

    const paths = ( Array.isArray( url ) ? url : [ url ] ) as string[];

    const results = paths.map(
        path =>
            new Promise( ( resolve, reject ) => {
                proto.load(
                    path,
                    ( data ) => {
                        if ( data.scene ) {
                            Object.assign( data, traverseObjects( data.scene ) );
                        }
                        resolve( data );
                    },
                    onProgress,
                    ( error: ErrorEvent ) => reject( logError( '[useLoader] - Failed to load resource', error as unknown as Error ) ),
                );
            } ),
    );

    return ( Array.isArray( url ) ? await Promise.all( results ) : await results[ 0 ] ) as U extends any[]
        ? LoaderReturnType<T, T>[]
        : LoaderReturnType<T, T>;
}
